//*****************************************************************************
// JFab (http://code.google.com/p/jfab)
// Copyright (c) 2011-2012 JFab.org
// Admin jfab@jeffreythompson.net
//
// See the file "LICENSE.txt" for information on usage and redistribution of
// this file, and for a DISCLAIMER OF ALL WARRANTIES.
//*****************************************************************************

package org.jfab.swingui.editpane;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeListener;
import java.util.List;

import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.SwingPropertyChangeSupport;
import javax.swing.text.JTextComponent;

import org.apache.commons.lang3.Validate;
import org.jfab.core.adapter.Adapter;
import org.jfab.core.adapter.ListAdapter;
import org.jfab.core.adapter.StringAdapter;
import org.jfab.swingui.util.GridLayout2;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Provides base functionality for an edit pane for a generic object.
 * 
 * @param <T> Subject type.
 */
public abstract class AbstractEditPane<T> extends JPanel implements EditPane<T>
{
    /** Default text field length. */
    protected static final int TEXT_FIELD_LENGTH = 30;

    /** Data property name. */
    private static final String DATA_PROPERTY = "data";

    /** Logger. */
    private static final Logger LOGGER = LoggerFactory
            .getLogger(AbstractEditPane.class);

    /** Serial version UID. */
    private static final long serialVersionUID = 1L;

    /** Flag indicating if data has been changed. */
    private boolean _isDirty;

    /** Flag indicating if we're initializing. */
    private boolean _isInitializing;

    /** Property change support. */
    private SwingPropertyChangeSupport _propertyChangeSupport;

    /** List&lt;String&gt; adapter. */
    private Adapter<List<String>> _stringListAdapter;

    /** Subject object. */
    private final T _subjectObject;

    /**
     * Construct this object with the given parameter.
     * 
     * @param subjectObject Subject object.
     */
    public AbstractEditPane(final T subjectObject)
    {
        setInitializing(true);

        Validate.notNull(subjectObject);

        _subjectObject = subjectObject;

        final int hgap = 5;
        final int vgap = 5;

        setLayout(new GridLayout2(0, 2, hgap, vgap));

        createWidgets();
        fillPanel();
        fillWidgets();

        setInitializing(false);
    }

    @Override
    public void addDataListener(final PropertyChangeListener listener)
    {
        getPropertyChangeSupport().addPropertyChangeListener(DATA_PROPERTY,
                listener);
    }

    @Override
    public void applyChanges()
    {
        setDirty(false);
    }

    @Override
    public T getSubjectObject()
    {
        return _subjectObject;
    }

    @Override
    public boolean isDirty()
    {
        return _isDirty;
    }

    @Override
    public void removeDataListener(final PropertyChangeListener listener)
    {
        getPropertyChangeSupport().removePropertyChangeListener(DATA_PROPERTY,
                listener);
    }

    /**
     * @param newValue New value.
     */
    void fireDataChange(final boolean newValue)
    {
        final boolean oldValue = !newValue;
        getPropertyChangeSupport().firePropertyChange(DATA_PROPERTY, oldValue,
                newValue);
    }

    /**
     * @param newValue New value.
     */
    void fireDataChange(final Object newValue)
    {
        final String oldValue = null;
        getPropertyChangeSupport().firePropertyChange(DATA_PROPERTY, oldValue,
                newValue);
    }

    /**
     * @param propertyName Property name.
     * @param component Text component.
     */
    protected void bindAndAdd(final String propertyName,
            final JComponent component)
    {
        bindAndAdd(propertyName, component, null);
    }

    /**
     * @param propertyName Property name.
     * @param component Text component.
     * @param adapter Adapter.
     */
    protected void bindAndAdd(final String propertyName,
            final JComponent component, final Adapter<?> adapter)
    {
        Validate.notNull(component, "component is null");

        if (component instanceof JTextComponent)
        {
            bindProperty(propertyName, (JTextComponent)component, adapter);
        }
        else if (component instanceof JCheckBox)
        {
            bindProperty(propertyName, (JCheckBox)component, adapter);
        }
        else if (component instanceof JLabel)
        {
            bindProperty(propertyName, (JLabel)component, adapter);
        }
        else
        {
            handleException(new RuntimeException(
                    "Binding not supported for type "
                            + component.getClass().getName()));
        }

        final String label = propertyName;
        addFormElements(label, component);
    }

    /**
     * @return a new check box widget.
     */
    protected JCheckBox createCheckBox()
    {
        return new JCheckBox();
    }

    /**
     * @return a new label widget.
     */
    protected JLabel createLabel()
    {
        return new JLabel();
    }

    /**
     * @return a new text area widget.
     */
    protected JTextArea createTextArea()
    {
        return new JTextArea(3, TEXT_FIELD_LENGTH);
    }

    /**
     * @return a new text field widget.
     */
    protected JTextField createTextField()
    {
        return new JTextField(TEXT_FIELD_LENGTH);
    }

    /**
     * Create the widgets.
     */
    protected abstract void createWidgets();

    /**
     * Fill this panel with widgets.
     */
    protected abstract void fillPanel();

    /**
     * Fill the widgets with data.
     */
    protected abstract void fillWidgets();

    /**
     * @return the propertyChangeSupport
     */
    protected SwingPropertyChangeSupport getPropertyChangeSupport()
    {
        if (_propertyChangeSupport == null)
        {
            _propertyChangeSupport = new SwingPropertyChangeSupport(this, true);
        }

        return _propertyChangeSupport;
    }

    /**
     * @return the stringListAdapter
     */
    protected Adapter<List<String>> getStringListAdapter()
    {
        if (_stringListAdapter == null)
        {
            _stringListAdapter = new ListAdapter<String>(new StringAdapter());
        }

        return _stringListAdapter;
    }

    /**
     * @return the subject class.
     */
    protected Class<?> getSubjectClass()
    {
        return getSubjectObject().getClass();
    }

    /**
     * Handle the given exception.
     * 
     * @param e Exception.
     */
    protected void handleException(final Exception e)
    {
        LOGGER.error(e.getMessage(), e);

        final String prefixMessage = "Exception thrown:";
        JOptionPane.showMessageDialog(null, prefixMessage + e.getMessage(),
                "Error", JOptionPane.ERROR_MESSAGE);
    }

    /**
     * @return the isInitializing
     */
    protected boolean isInitializing()
    {
        return _isInitializing;
    }

    /**
     * @param isDirty the isDirty to set
     */
    protected void setDirty(final boolean isDirty)
    {
        _isDirty = isDirty;
    }

    /**
     * @param isInitializing the isInitializing to set
     */
    protected void setInitializing(final boolean isInitializing)
    {
        _isInitializing = isInitializing;
    }

    /**
     * @param checkBox Check box widget.
     * @param value Value.
     */
    protected void setValue(final JCheckBox checkBox, final Boolean value)
    {
        if (value != null)
        {
            checkBox.setSelected(value.booleanValue());
        }
        else
        {
            checkBox.setSelected(false);
        }
    }

    /**
     * @param label Label.
     * @param component Component.
     */
    private void addFormElements(final String label, final JComponent component)
    {
        add(new JLabel(label));

        if (component instanceof JTextArea)
        {
            add(new JScrollPane(component));
        }
        else
        {
            add(component);
        }
    }

    /**
     * @param propertyName Property name.
     * @param component Text component.
     * @param adapter Adapter.
     */
    private void bindProperty(final String propertyName,
            final JCheckBox component, final Adapter<?> adapter)
    {
        component.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(final ActionEvent event)
            {
                if (!isInitializing())
                {
                    final boolean newValue = component.isSelected();
                    setDirty(true);
                    fireDataChange(newValue);
                }
            }
        });
    }

    /**
     * @param propertyName Property name.
     * @param component Text component.
     * @param adapter Adapter.
     */
    private void bindProperty(final String propertyName,
            final JLabel component, final Adapter<?> adapter)
    {
        // Nothing to do.
    }

    /**
     * @param propertyName Property name.
     * @param component Text component.
     * @param adapter Adapter.
     */
    private void bindProperty(final String propertyName,
            final JTextComponent component, final Adapter<?> adapter)
    {
        component.getDocument().addDocumentListener(new DocumentListener()
        {
            @Override
            public void changedUpdate(final DocumentEvent event)
            {
                update(event);
            }

            @Override
            public void insertUpdate(final DocumentEvent event)
            {
                update(event);
            }

            @Override
            public void removeUpdate(final DocumentEvent event)
            {
                update(event);
            }

            /**
             * @param event Event.
             */
            private void update(final DocumentEvent event)
            {
                if (!isInitializing())
                {
                    final String text = component.getText();
                    setDirty(true);
                    fireDataChange(text);
                }
            }
        });
    }
}
